Simplifying Parameter Validation with CallerArgumentExpression
When developing packages, to ensure that constructor or method parameters meet expectations, we typically perform parameter validation for null or empty strings. To simplify operations and unify error messages, I usually write a static ExceptionUtils class to perform these checks, as shown in the example below:
public static class ExceptionUtils {
public static void ThrowIfNull<T>(Expression<Func<T?>> expression) {
_ = expression.Compile().Invoke()
?? throw new ArgumentNullException(GetMemberName(expression));
}
public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression) {
string? value = expression.Compile().Invoke();
if (string.IsNullOrWhiteSpace(value)) {
throw new ArgumentException("Must not be null or whitespace.", GetMemberName(expression));
}
}
private static string GetMemberName<T>(Expression<Func<T>> expression) {
if (expression.Body is not MemberExpression expressionBody) {
throw new ArgumentException("Invalid expression.", nameof(expression));
}
return expressionBody.Member.Name;
}
}This allows for parameter checking in the following way. Using Expression avoids the need to pass both the parameter value and the parameter name, simplifying usage:
public void Method(string str) {
ExceptionUtils.ThrowIfNullOrWhiteSpace(() => str);
}However, .NET 6 introduced the Nullable reference type checking mechanism. Typically, after we perform a null check, the compiler can recognize that the variable will not be null:
string ToLower(string? str) {
if (str is null) {
throw new ArgumentNullException(nameof(str));
}
// Since null has been checked, the compiler will no longer issue null warnings for str
return str.ToLower();
}But because my ExceptionUtils uses Expression rather than checking the parameter directly, I cannot add [NotNull] to the parameter to let the compiler know that the checked parameter is not null. Therefore, I adjusted the code as follows:
public static class ExceptionUtils {
public static void ThrowIfNull<T>(Expression<Func<T>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
_ = expression.Compile().Invoke()
?? throw new ArgumentNullException(GetMemberName(expression));
}
public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
string? value = expression.Compile().Invoke();
if (string.IsNullOrWhiteSpace(value)) {
throw new ArgumentException("Must not be null or whitespace.", GetMemberName(expression));
}
}
private static string GetMemberName<T>(Expression<Func<T>> expression) {
return expression.Body is not MemberExpression expressionBody
? throw new ArgumentException("Invalid expression.", nameof(expression))
: expressionBody.Member.Name;
}
}The isNull parameter is only there because using [DoesNotReturn] directly triggers the warning: "Methods marked with [DoesNotReturn] should not return." I had to use [DoesNotReturnIf(true)] combined with the meaningless isNull parameter to handle it. Of course, I have never been very satisfied with the above solution. Since .NET 6 and .NET 7, the official team has provided some simplified static check methods:
// Added in .NET 6
ArgumentNullException.ThrowIfNull(object? argument, string? paramName = null);
// Added in .NET 7
ArgumentNullException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentNullException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);
// Added in .NET 7
ArgumentException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);Recently, I looked at the source code for ThrowIfNull, and the [CallerArgumentExpression] within it reminded me of a book I borrowed from a junior colleague, which mentioned that it is used to automatically retrieve parameter names.
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
if (argument is null) {
Throw(paramName);
}
}Therefore, I wrote the following program to test it:
string? str = "";
Console.Write("When paramName is not passed, ");
TestCallerArgumentExpression(str);
Console.Write("When paramName is passed, ");
TestCallerArgumentExpression(str, "str2");
void TestCallerArgumentExpression(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
Console.WriteLine("paramName:" + paramName);
}The results are as follows:
When paramName is not passed, paramName: str
When paramName is passed, paramName: str2When paramName is not passed, it automatically uses the variable name of the argument passed in as the value for paramName. This approach is more concise than my original Expression solution, and it also allows the use of [NotNull] to support Nullable reference checks.
Change Log
- 2024-10-13 Initial documentation created.